package club.jdiy.core.base;

import club.jdiy.core.base.domain.*;
import org.springframework.data.domain.*;
import org.springframework.data.jpa.convert.QueryByExamplePredicateBuilder;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.data.jpa.provider.PersistenceProvider;
import org.springframework.data.jpa.repository.query.EscapeCharacter;
import org.springframework.data.jpa.repository.query.QueryUtils;
import org.springframework.data.jpa.repository.support.CrudMethodMetadata;
import org.springframework.data.jpa.repository.support.JpaEntityInformation;
import org.springframework.data.jpa.repository.support.JpaEntityInformationSupport;
import org.springframework.data.jpa.repository.support.JpaRepositoryImplementation;
import org.springframework.data.repository.NoRepositoryBean;
import org.springframework.data.repository.support.PageableExecutionUtils;
import org.springframework.lang.Nullable;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.Assert;

import javax.persistence.*;
import javax.persistence.criteria.*;
import java.util.*;

import static org.springframework.data.jpa.repository.query.QueryUtils.*;
import static org.springframework.data.jpa.repository.query.QueryUtils.toOrders;

/**
 * JDiy Dao 接口层通用方法的默认实现
 * @param <T> Entity 类
 * @param <ID> 实体类的主键类型
 *
 * @author ziquee(子秋) QQ号:39886616 主页: http://www.jdiy.club
 * @version 20210609
 */
@NoRepositoryBean
@SuppressWarnings("NullableProblems")
public class JDiyBaseRepository<T extends JpaEntity<ID>, ID> extends RepositoryBasic<T, ID> implements JpaDyn<T, ID>, JpaRepositoryImplementation<T, ID> {
    private final JpaEntityInformation<T, ?> entityInformation;
    private final EntityManager em;
    private final PersistenceProvider provider;
    private EscapeCharacter escapeCharacter = EscapeCharacter.DEFAULT;

    public JDiyBaseRepository(JpaEntityInformation<T, ?> entityInformation, EntityManager entityManager) {
        Assert.notNull(entityInformation, "JpaEntityInformation must not be null!");
        Assert.notNull(entityManager, "EntityManager must not be null!");

        super.init(entityInformation, entityManager);
        this.entityInformation = entityInformation;
        this.em = entityManager;
        this.provider = PersistenceProvider.fromEntityManager(entityManager);
    }

    @SuppressWarnings("unused")
    public JDiyBaseRepository(Class<T> domainClass, EntityManager em) {
        this(JpaEntityInformationSupport.getEntityInformation(domainClass, em), em);
    }

    @Override
    public void setRepositoryMethodMetadata(CrudMethodMetadata crudMethodMetadata) {
        this.metadata = crudMethodMetadata;
    }

    @Override
    public void setEscapeCharacter(EscapeCharacter escapeCharacter) {
        this.escapeCharacter = escapeCharacter;
    }

    protected Class<T> getDomainClass() {
        return entityInformation.getJavaType();
    }

    private String getDeleteAllQueryString() {
        return getQueryString(DELETE_ALL_QUERY_STRING, entityInformation.getEntityName());
    }

    private String getCountQueryString() {
        String countQuery = String.format(COUNT_QUERY_STRING, provider.getCountQueryPlaceholder(), "%s");
        return getQueryString(countQuery, entityInformation.getEntityName());
    }

    @Transactional
    @Override
    public void deleteInBatch(Iterable<T> entities) {
        Assert.notNull(entities, "Entities must not be null!");
        if (!entities.iterator().hasNext()) {
            return;
        }

        applyAndBind(getQueryString(DELETE_ALL_QUERY_STRING, entityInformation.getEntityName()), entities, em)
                .executeUpdate();
    }

    @Transactional
    @Override
    public void deleteAll() {

        for (T element : findAll()) {
            delete(element);
        }
    }

    @Transactional
    @Override
    public void deleteAllInBatch() {
        em.createQuery(getDeleteAllQueryString()).executeUpdate();
    }

    protected org.springframework.data.jpa.repository.support.QueryHints getQueryHints() {
        return metadata == null ? org.springframework.data.jpa.repository.support.QueryHints.NoHints.INSTANCE
                : DefaultQueryHints.of(entityInformation, metadata);
    }

    @Override
    public List<T> findAll() {
        return findAll(Sort.unsorted());
    }

    @Override
    public List<T> findAll(Sort sort) {
        return getQuery(null, sort).getResultList();
    }

    @Override
    public Page<T> findAll(Pageable pageable) {

        if (isUnpaged(pageable)) {
            return new PageImpl<>(findAll());
        }

        return findAll((Specification<T>) null, pageable);
    }

    @Override
    public Optional<T> findOne(@Nullable Specification<T> spec) {

        try {
            return Optional.of(getQuery(spec, Sort.unsorted()).getSingleResult());
        } catch (NoResultException e) {
            return Optional.empty();
        }
    }

    @Override
    public List<T> findAll(@Nullable Specification<T> spec) {
        return getQuery(spec, Sort.unsorted()).getResultList();
    }

    @Override
    public Page<T> findAll(@Nullable Specification<T> spec, Pageable pageable) {

        TypedQuery<T> query = getQuery(spec, pageable);
        return isUnpaged(pageable) ? new PageImpl<>(query.getResultList())
                : readPage(query, getDomainClass(), pageable, spec);
    }

    @Override
    public List<T> findAll(@Nullable Specification<T> spec, Sort sort) {
        return getQuery(spec, sort).getResultList();
    }

    @Override
    public <S extends T> Optional<S> findOne(Example<S> example) {

        try {
            return Optional
                    .of(getQuery(new JDiyBaseRepository.ExampleSpecification<>(example, escapeCharacter), example.getProbeType(), Sort.unsorted())
                            .getSingleResult());
        } catch (NoResultException e) {
            return Optional.empty();
        }
    }

    @Override
    public <S extends T> long count(Example<S> example) {
        return executeCountQuery(
                getCountQuery(new JDiyBaseRepository.ExampleSpecification<>(example, escapeCharacter), example.getProbeType()));
    }

    @Override
    public <S extends T> boolean exists(Example<S> example) {
        return !getQuery(new JDiyBaseRepository.ExampleSpecification<>(example, escapeCharacter), example.getProbeType(), Sort.unsorted())
                .getResultList().isEmpty();
    }

    @Override
    public <S extends T> List<S> findAll(Example<S> example) {
        return getQuery(new JDiyBaseRepository.ExampleSpecification<>(example, escapeCharacter), example.getProbeType(), Sort.unsorted())
                .getResultList();
    }

    @Override
    public <S extends T> List<S> findAll(Example<S> example, Sort sort) {
        return getQuery(new JDiyBaseRepository.ExampleSpecification<>(example, escapeCharacter), example.getProbeType(), sort)
                .getResultList();
    }

    @Override
    public <S extends T> Page<S> findAll(Example<S> example, Pageable pageable) {

        JDiyBaseRepository.ExampleSpecification<S> spec = new JDiyBaseRepository.ExampleSpecification<>(example, escapeCharacter);
        Class<S> probeType = example.getProbeType();
        TypedQuery<S> query = getQuery(new JDiyBaseRepository.ExampleSpecification<>(example, escapeCharacter), probeType, pageable);

        return isUnpaged(pageable) ? new PageImpl<>(query.getResultList()) : readPage(query, probeType, pageable, spec);
    }

    @Override
    public long count() {
        return em.createQuery(getCountQueryString(), Long.class).getSingleResult();
    }

    @Override
    public long count(@Nullable Specification<T> spec) {
        return executeCountQuery(getCountQuery(spec, getDomainClass()));
    }



    @Transactional
    public <S extends T> List<S> saveAll(Iterable<S> entities) {
        Assert.notNull(entities, "Entities must not be null!");
        List<S> result = new ArrayList();
        Iterator var3 = entities.iterator();

        while(var3.hasNext()) {
            S entity =(S) var3.next();
            result.add(this.save(entity));
        }

        return result;
    }

    @Transactional
    public <S extends T> List<S> saveAllAndFlush(Iterable<S> entities) {
        List<S> result = this.saveAll(entities);
        this.flush();
        return result;
    }
    @Transactional
    public void deleteAllByIdInBatch(Iterable<ID> ids) {
        Assert.notNull(ids, "Ids must not be null!");
        if (ids.iterator().hasNext()) {
            String queryString = String.format("delete from %s x where %s in :ids", this.entityInformation.getEntityName(), this.entityInformation.getIdAttribute().getName());
            Query query = this.em.createQuery(queryString);
            query.setParameter("ids", ids);
            query.executeUpdate();
        }
    }
    @Transactional
    public void deleteAllById(Iterable<? extends ID> ids) {
        Assert.notNull(ids, "Ids must not be null!");
        Iterator var2 = ids.iterator();

        while(var2.hasNext()) {
            ID id =(ID) var2.next();
            this.deleteById(id);
        }

    }
    @Transactional
    public void deleteAllInBatch(Iterable<T> entities) {
        Assert.notNull(entities, "Entities must not be null!");
        if (entities.iterator().hasNext()) {
            QueryUtils.applyAndBind(QueryUtils.getQueryString("delete from %s x", this.entityInformation.getEntityName()), entities, this.em).executeUpdate();
        }
    }


    @Transactional
    @Override
    public <S extends T> S saveAndFlush(S entity) {

        S result = save(entity);
        flush();

        return result;
    }

    @Transactional
    @Override
    public void flush() {
        em.flush();
    }

    @Deprecated
    protected Page<T> readPage(TypedQuery<T> query, Pageable pageable, @Nullable Specification<T> spec) {
        return readPage(query, getDomainClass(), pageable, spec);
    }

    protected <S extends T> Page<S> readPage(TypedQuery<S> query, final Class<S> domainClass, Pageable pageable,
                                             @Nullable Specification<S> spec) {

        if (pageable.isPaged()) {
            query.setFirstResult((int) pageable.getOffset());
            query.setMaxResults(pageable.getPageSize());
        }

        return PageableExecutionUtils.getPage(query.getResultList(), pageable,
                () -> executeCountQuery(getCountQuery(spec, domainClass)));
    }

    protected TypedQuery<T> getQuery(@Nullable Specification<T> spec, Pageable pageable) {

        Sort sort = pageable.isPaged() ? pageable.getSort() : Sort.unsorted();
        return getQuery(spec, getDomainClass(), sort);
    }

    protected <S extends T> TypedQuery<S> getQuery(@Nullable Specification<S> spec, Class<S> domainClass,
                                                   Pageable pageable) {

        Sort sort = pageable.isPaged() ? pageable.getSort() : Sort.unsorted();
        return getQuery(spec, domainClass, sort);
    }

    protected TypedQuery<T> getQuery(@Nullable Specification<T> spec, Sort sort) {
        return getQuery(spec, getDomainClass(), sort);
    }

    protected <S extends T> TypedQuery<S> getQuery(@Nullable Specification<S> spec, Class<S> domainClass, Sort sort) {

        CriteriaBuilder builder = em.getCriteriaBuilder();
        CriteriaQuery<S> query = builder.createQuery(domainClass);

        Root<S> root = applySpecificationToCriteria(spec, domainClass, query);
        query.select(root);

        if (sort.isUnsorted() && Sortable.class.isAssignableFrom(getDomainClass())) {
            sort = Sort.by(Sort.Order.asc("sortIndex"));
        }

        if (sort.isSorted()) {
            query.orderBy(toOrders(sort, root, builder));
        }

        return applyRepositoryMethodMetadata(em.createQuery(query));
    }

    @Deprecated
    protected TypedQuery<Long> getCountQuery(@Nullable Specification<T> spec) {
        return getCountQuery(spec, getDomainClass());
    }

    protected <S extends T> TypedQuery<Long> getCountQuery(@Nullable Specification<S> spec, Class<S> domainClass) {

        CriteriaBuilder builder = em.getCriteriaBuilder();
        CriteriaQuery<Long> query = builder.createQuery(Long.class);

        Root<S> root = applySpecificationToCriteria(spec, domainClass, query);

        if (query.isDistinct()) {
            query.select(builder.countDistinct(root));
        } else {
            query.select(builder.count(root));
        }
        query.orderBy(Collections.emptyList());

        return em.createQuery(query);
    }

    private <S, U extends T> Root<U> applySpecificationToCriteria(@Nullable Specification<U> spec, Class<U> domainClass,
                                                                  CriteriaQuery<S> query) {

        Assert.notNull(domainClass, "Domain class must not be null!");
        Assert.notNull(query, "CriteriaQuery must not be null!");

        Root<U> root = query.from(domainClass);

        if (spec == null) {
            return root;
        }

        CriteriaBuilder builder = em.getCriteriaBuilder();
        Predicate predicate = spec.toPredicate(root, query, builder);

        if (predicate != null) {
            query.where(predicate);
        }

        return root;
    }

    private <S> TypedQuery<S> applyRepositoryMethodMetadata(TypedQuery<S> query) {

        if (metadata == null) {
            return query;
        }

        LockModeType type = metadata.getLockModeType();
        TypedQuery<S> toReturn = type == null ? query : query.setLockMode(type);

        applyQueryHints(toReturn);

        return toReturn;
    }

    private void applyQueryHints(Query query) {
        this.getQueryHints().withFetchGraphs(this.em).forEach(query::setHint);
    }

    private static long executeCountQuery(TypedQuery<Long> query) {
        Assert.notNull(query, "TypedQuery must not be null!");

        List<Long> totals = query.getResultList();
        long total = 0L;

        for (Long element : totals) {
            total += element == null ? 0 : element;
        }
        return total;
    }

    private static boolean isUnpaged(Pageable pageable) {
        return pageable.isUnpaged();
    }

    private static class ExampleSpecification<T> implements Specification<T> {

        private static final long serialVersionUID = 1L;

        private final Example<T> example;
        private final EscapeCharacter escapeCharacter;

        ExampleSpecification(Example<T> example, EscapeCharacter escapeCharacter) {

            Assert.notNull(example, "Example must not be null!");
            Assert.notNull(escapeCharacter, "EscapeCharacter must not be null!");

            this.example = example;
            this.escapeCharacter = escapeCharacter;
        }

        @Override
        public Predicate toPredicate(Root<T> root, CriteriaQuery<?> query, CriteriaBuilder cb) {
            return QueryByExamplePredicateBuilder.getPredicate(root, cb, example, escapeCharacter);
        }
    }
}
